Coverage Report

Created: 2026-04-26 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\xtask\src\changelog.rs
Line
Count
Source
1
//! Changelog generation via the external `changelogging` tool.
2
//!
3
//! Reads the current version from `Cargo.toml`, synchronises it into
4
//! `changelogging.toml`, then invokes `changelogging build --remove` to
5
//! consume news fragments and append an entry to `CHANGELOG.md`.
6
7
use anyhow::{Context, Result};
8
9
/// All side-effecting operations required by this module.
10
///
11
/// Implement with mocks in tests to achieve zero filesystem and process
12
/// side-effects.
13
pub trait ChangelogSystem {
14
    /// Read the contents of `Cargo.toml`.
15
    ///
16
    /// # Errors
17
    ///
18
    /// Returns an error if the file cannot be read.
19
    fn read_cargo_toml(&self) -> Result<String>;
20
21
    /// Read the contents of `changelogging.toml`.
22
    ///
23
    /// # Errors
24
    ///
25
    /// Returns an error if the file cannot be read.
26
    fn read_changelogging_toml(&self) -> Result<String>;
27
28
    /// Write `content` to `changelogging.toml`.
29
    ///
30
    /// # Errors
31
    ///
32
    /// Returns an error if the write fails.
33
    fn write_changelogging_toml(&self, content: &str) -> Result<()>;
34
35
    /// Run `changelogging build --remove` to generate `CHANGELOG.md`.
36
    ///
37
    /// # Errors
38
    ///
39
    /// Returns an error if the process cannot be started or exits non-zero.
40
    fn run_changelogging_build(&self) -> Result<()>;
41
}
42
43
/// Production implementation of [`ChangelogSystem`].
44
pub struct RealSystem;
45
46
#[cfg_attr(coverage_nightly, coverage(off))]
47
impl ChangelogSystem for RealSystem {
48
    fn read_cargo_toml(&self) -> Result<String> {
49
        std::fs::read_to_string("Cargo.toml").context("failed to read Cargo.toml")
50
    }
51
52
    fn read_changelogging_toml(&self) -> Result<String> {
53
        std::fs::read_to_string("changelogging.toml").context("failed to read changelogging.toml")
54
    }
55
56
    fn write_changelogging_toml(&self, content: &str) -> Result<()> {
57
        std::fs::write("changelogging.toml", content).context("failed to write changelogging.toml")
58
    }
59
60
    fn run_changelogging_build(&self) -> Result<()> {
61
        let status = std::process::Command::new("changelogging")
62
            .args(["build", "--remove"])
63
            .status()
64
            .context("failed to run `changelogging build --remove`")?;
65
        if !status.success() {
66
            anyhow::bail!("`changelogging build --remove` exited with status {status}");
67
        }
68
        Ok(())
69
    }
70
}
71
72
/// Extract the `[package].version` value from a `Cargo.toml` string.
73
///
74
/// # Arguments
75
///
76
/// * `cargo_toml_content` - Raw TOML text of `Cargo.toml`.
77
///
78
/// # Returns
79
///
80
/// The version string (e.g. `"0.18.1"`).
81
///
82
/// # Errors
83
///
84
/// Returns an error if the content cannot be parsed as TOML or the
85
/// `[package].version` key is absent.
86
13
pub fn extract_version_from_cargo_toml(cargo_toml_content: &str) -> Result<String> {
87
13
    let 
doc12
:
toml_edit::Document12
= cargo_toml_content
88
13
        .parse()
89
13
        .context("failed to parse Cargo.toml")
?1
;
90
12
    let 
version11
= doc
91
12
        .get("package")
92
12
        .and_then(|p| p.as_table())
93
12
        .and_then(|t| t.get("version"))
94
12
        .and_then(|v| 
v11
.
as_str11
())
95
12
        .context("missing [package].version in Cargo.toml")
?1
;
96
11
    Ok(version.to_owned())
97
13
}
98
99
/// Set `context.version` in a `changelogging.toml` document to `version`.
100
///
101
/// All other keys and formatting are preserved via `toml_edit`.
102
///
103
/// # Arguments
104
///
105
/// * `changelogging_content` - Raw TOML text of `changelogging.toml`.
106
/// * `version` - Version string to write.
107
///
108
/// # Returns
109
///
110
/// Updated TOML text.
111
///
112
/// # Errors
113
///
114
/// Returns an error if `changelogging_content` cannot be parsed as TOML.
115
4
pub fn set_changelogging_version(changelogging_content: &str, version: &str) -> Result<String> {
116
4
    let mut doc: toml_edit::Document = changelogging_content
117
4
        .parse()
118
4
        .context("failed to parse changelogging.toml")
?0
;
119
4
    doc["context"]["version"] = toml_edit::value(version);
120
4
    Ok(doc.to_string())
121
4
}
122
123
/// Generate the changelog for the version currently recorded in `Cargo.toml`.
124
///
125
/// 1. Reads the version from `Cargo.toml`.
126
/// 2. Rewrites `changelogging.toml` with the new version.
127
/// 3. Runs `changelogging build --remove`.
128
///
129
/// # Arguments
130
///
131
/// * `system` - Injected I/O provider.
132
///
133
/// # Errors
134
///
135
/// Returns an error if any step fails.
136
2
pub fn generate_changelog<S: ChangelogSystem>(system: &S) -> Result<()> {
137
2
    let cargo_toml = system.read_cargo_toml()
?0
;
138
2
    let version = extract_version_from_cargo_toml(&cargo_toml)
?0
;
139
2
    println!("Generating changelog for version {version}");
140
141
2
    let changelogging_toml = system.read_changelogging_toml()
?0
;
142
2
    let updated = set_changelogging_version(&changelogging_toml, &version)
?0
;
143
2
    system.write_changelogging_toml(&updated)
?0
;
144
145
2
    system.run_changelogging_build()
?0
;
146
2
    Ok(())
147
2
}
148
149
#[cfg(test)]
150
#[path = "tests/test_changelog.rs"]
151
mod tests;